<?php


namespace coco\client;

use coco\Config;
use PDO;
use PDOException;
use PDOStatement;
use Throwable;

/**
 * Class Mysql
 * 一个实例就是一个链接
 *
 * @property-read bool $connected
 * @property-read string $connect_error
 * @property-read int $connect_errno
 * @property-read string $error
 * @property-read int $errno
 * @property-read int $affected_rows
 * @property-read mixed $insert_id
 * @property-read string $sql
 * @package coco\client
 */
class Mysql
{
    //只读属性
    protected $option;
    protected $connected = false;
    protected $connect_error = '';
    protected $connect_errno = 0;
    protected $error = '';
    protected $errno = 0;
    protected $affected_rows = 0;
    protected $insert_id = 0;
    protected $connect_time = 0;
    protected $reconnect_times = 0;
    /**@var PDO $pdo */
    protected $pdo = null;
    protected $sql = '';

    public function __get($name)
    {
        if (!in_array($name, [
            'config',
            'connected',
            'connect_error',
            'connect_errno',
            'error',
            'errno',
            'affected_rows',
            'insert_id',
            'sql',
        ])) {
            return null;
        }
        return $this->$name;
    }

    public function startTrans()
    {
        $this->pdo->beginTransaction();
    }

    public function commit()
    {
        $this->pdo->commit();
    }

    public function rollBack()
    {
        $this->pdo->rollBack();
    }

    public function __construct(array $option = [])
    {
        $this->option = $option;
        $this->reconnect();
    }

    //方法部分

    /**
     * @return bool
     */
    public function reconnect(): bool
    {
        if ($this->pdo instanceof PDO) {
            $this->pdo = null;
        }

        $db = Config::getInstance()->get('db');
        $config = [
            'database' => $this->option['database'] ?? $db['database'] ?? 'test',
            'host' => $this->option['host'] ?? $db['host'] ?? '127.0.0.1',
            'port' => $this->option['port'] ?? $db['port'] ?? 3306,
            'options' => $this->option['options'] ?? $db['options'] ?? [],
            'charset' => $this->option['charset'] ?? $db['charset'] ?? 'utf8',
            'username' => $this->option['username'] ?? $db['username'] ?? 'root',
            'password' => $this->option['password'] ?? $db['password'] ?? '',
        ];
        $reconnect_times = $this->option['reconnect'] ?? $db['reconnect'] ?? 10;
        if ($this->reconnect_times++ > $reconnect_times) {
            return false;
        }
        $dns = sprintf("mysql:dbname=%s;host=%s:%d", $config['database'], $config['host'], $config['port']);
        //用array_merge会改键值 + 号能被覆盖
        $options = (array)$config['options'] + [
                PDO::ATTR_CASE => PDO::CASE_LOWER,
                PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL,
                PDO::ATTR_STRINGIFY_FETCHES => false,
                PDO::ATTR_EMULATE_PREPARES => false,
                PDO::ATTR_TIMEOUT => 30,
            ];
        //两个方式设置字符集
        $options[PDO::MYSQL_ATTR_INIT_COMMAND] = 'SET NAMES ' . $config['charset'];
        $dns .= ';charset=' . $config['charset'];
        try {
            $this->pdo = new PDO($dns, $config['username'], $config['password'], $options);
        } catch (PDOException $e) {
            $this->errno = $this->connect_errno = $e->getCode();
            $this->error = $this->connect_error = $e->getMessage();
            $this->pdo = null;
            return false;
        }
        $this->connected = true;
        $this->connect_time = 0;
        return true;
    }

    /**
     * @param string $sql
     * @param array $params
     * @return array|bool
     */
    public function query(string $sql, array $params = [])
    {
        if (false === $stat = $this->prepare($sql)) {
            return false;
        }
        if (false === $stat = $this->runWithParams($stat, $params)) {
            return false;
        }
        foreach ($params as $k => $v) {
            if (is_string($v)) {
                $v = '"' . addslashes($v) . '"';
            }
            $sql = str_replace($k, $v, $sql);
        }
        $this->sql = $sql;
        return $stat->fetchAll(PDO::FETCH_ASSOC);
    }


    /**
     * @param string $sql
     * @param array|null $params
     * @return int|bool
     */
    public function execute(string $sql, array $params = [])
    {
        if (false === $stat = $this->prepare($sql)) {
            return false;
        }
        if (false === $stat = $this->runWithParams($stat, $params)) {
            return false;
        }
        $this->affected_rows = $stat->rowCount();
        if (preg_match("/^\s*(INSERT\s+INTO|REPLACE\s+INTO)\s+/i", $sql)) {
            $this->insert_id = $this->pdo->lastInsertId();
        }
        foreach ($params as $k => $v) {
            if (is_string($v)) {
                $v = '"' . addslashes($v) . '"';
            }
            $sql = str_replace($k, $v, $sql);
        }
        $this->sql = $sql;
        return $this->affected_rows;
    }


    /**
     * @param string $sql
     * @return bool|PDOStatement
     */
    public function prepare(string $sql)
    {
        if (!$this->connected) {
            return false;
        }
        //坑啊 没说会抛出Pdo异常啊
        try {
            if (false === $stat = $this->pdo->prepare($sql)) {
                $this->errno = $this->pdo->errorCode();
                $this->error = join(PHP_EOL, $this->pdo->errorInfo());
                return false;
            }
        } catch (PDOException $e) {
            $this->errno = $e->getCode();
            $this->error = $e->getMessage();
            return false;
        } catch (Throwable $e)
        {
            if($this->reconnect())
            {
                return $this->prepare($sql);
            }
            $this->errno = $e->getCode();
            $this->error = $e->getMessage();
            return false;
        }
         
        return $stat;
    }

    /**
     * @param PDOStatement $stat
     * @param array|null $params
     * @return bool|PDOStatement
     */
    protected function runWithParams(PDOStatement $stat, array $params = null)
    {
        if (!empty($params)) {
            foreach ($params as $key => $val) {
                $stat->bindValue($key, $val, is_string($val) ? PDO::PARAM_STR : PDO::PARAM_INT);
            }
        }
        try {
            if (false === $stat->execute()) {
                $this->errno = $stat->errorCode();
                $this->error = join(PHP_EOL, $stat->errorInfo());
                return false;
            }
        } catch (PDOException $e) {
            $this->errno = $e->getCode();
            $this->error = $e->getMessage();
            return false;
        }
        $this->connect_time = time();
        return $stat;
    }

    /**
     * @param $name
     * @param $arguments
     * @return false|mixed
     */
    public function __call($name, $arguments)
    {
        if (method_exists($this->option, $name)) {
            $keep_cd = $this->option['keep_cd'] ?? $config['keep_cd'] ?? 60;
            if ($this->connect_time < time() - $keep_cd) {
                $this->keep();
            }
            if ($this->connected) {
                $return = call_user_func_array([$this->pdo, $name], $arguments);
                $this->connect_time = time();
                return $return;
            }
        }
        throw new PDOException('fail : reconnect limit');
    }


    protected function keep()
    {
        if ($this->ping()) {
            $this->connect_time = time();
        } else {
            $this->reconnect();
            $this->connected = false;
        }
    }

    public function ping(): bool
    {
        try {
            $this->pdo->getAttribute(PDO::ATTR_SERVER_INFO);
        } catch (PDOException $e) {
            $this->connected = false;
            return false;
        }
        $this->connect_time = time();
        return true;
    }

}